#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sched.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>

#include "include/config_manager.h"
#include "include/logger.h"

#define TRUE                    1
#define FALSE                   0

struct {
    char *ext;
    char *filetype;
} supportedft[] = {
    {"gif", "image/gif"},
    {"jpg", "image/jpg"},
    {"jpeg", "image/jpeg"},
    {"png", "image/png"},
    {"ico", "image/ico"},
    {"zip", "application/zip"},
    {"gz", "application/x-gzip"},
    {"tar", "application/x-tar"},
    {"pdf", "application/pdf"},
    {"htm", "text/html"},
    {"html", "text/html"},
    {"txt", "text/plain"},
    {"css", "text/css"},
    {"js", "application/x-javascript"},
    {0, 0}
};

struct request{
    char *method;
    char *URI;
    char *httpver;
    char *host;
    char *connection;
    char *useragent;
    char *accept;
};

/* Converts a hex character to its integer value */
char from_hex(char ch) {
    return isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10;
}

/* Converts an integer value to its hex character*/
char to_hex(char code) {
    static char hex[] = "0123456789abcdef";
    return hex[code & 15];
}

/* Returns a url-decoded version of str */
/* IMPORTANT: be sure to free() the returned string after use */
char *url_decode(char *str) {
    char *pstr = str, *buf = malloc(strlen(str) + 1), *pbuf = buf;
    while (*pstr) {
        if (*pstr == '%') {
            if (pstr[1] && pstr[2]) {
                *pbuf++ = from_hex(pstr[1]) << 4 | from_hex(pstr[2]);
                pstr += 2;
            }
        } else if (*pstr == '+') { 
            *pbuf++ = ' ';
        } else {
            *pbuf++ = *pstr;
        }
        pstr++;
    }
    *pbuf = '\0';
    return buf;
}

int parseRequest(char *rcv_req, struct request *req)
{
    
    req->method = strtok(rcv_req, " ");
    req->URI = &strtok(NULL, " ")[1];
    req->httpver = strtok(NULL, "\n");
    char *temp_token;
    while ((temp_token = strtok(NULL, "\n")) != NULL) {
        if (strstr(temp_token, "Host: ") != NULL) {
            req->host = &temp_token[strlen("Host: ")];
        }
        else if (strstr(temp_token, "Connection: ") != NULL) {
            req->connection = &temp_token[strlen("Connection: ")];
        }
        else if (strstr(temp_token, "User-Agent: ") != NULL) {
            req->useragent = &temp_token[strlen("User-Agent: ")];
        }
        else if (strstr(temp_token, "Accept: ") != NULL) {
            req->useragent = &temp_token[strlen("Accept: ")];
        }
    }
    if (req->method == NULL || req->URI == NULL || req->httpver == NULL) {
        return -1;
    }
    return 1;
}

int open_fd (char *path, char *filename)
{
    if(strstr(filename, "..") != NULL){
        /* todo log */
        return -1;
    }
    char *temp = alloca(sizeof(char *));
    temp = url_decode(filename);
    int size = 1;
    size += strlen(path);
    size += strlen(temp);
    char file[size];
    strcpy(file, path);
    strcat(file, temp);
    return open(file, O_RDONLY, S_IREAD | S_IWRITE);
}

long sizeOfFile(int fd)
{
    long len;
    len = (long)lseek(fd, (off_t)0, SEEK_END);
    (void)lseek(fd, (off_t)0, SEEK_SET);
    return len;
}

char *detectFT(char *filename)
{
    char *FT = NULL;
    unsigned long fn_length = strlen(filename);
    int i = 0;
    
    for (i=0; supportedft[i].ext != 0; i++) {
        unsigned long len = strlen(supportedft[i].ext);
        if (!strncasecmp(&filename[fn_length-len], supportedft[i].ext, len)) {
            FT = supportedft[i].filetype;
            break;
        }
    }
    
    return FT;
}

void *threadJob(void * arg)
{
    char recv_buff[config_manager->buffer_size];
    char send_buff[config_manager->buffer_size];
    int fd;
    int socket = *(int *)arg;
    long f_length;
    char *f_type;
    int connection_close = FALSE;
    int file_not_found = FALSE;
    
    struct timeval tv_rcv;
    tv_rcv.tv_sec = 10;
    tv_rcv.tv_usec = 0;
    if(setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (struct timeval *)&tv_rcv, sizeof(struct timeval)) == -1){
        perror("Error during timeout configuration of connection socket");
        /* todo log */
        close(socket);
        pthread_exit(NULL);
    }
    struct timeval tv_send;
    tv_send.tv_sec = 4;
    tv_send.tv_usec = 0;
    if(setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, (struct timeval *)&tv_send, sizeof(struct timeval)) == -1){
        perror("Error during timeout configuration of connection socket");
        /* todo log */
        close(socket);
        pthread_exit(NULL);
    }
    
    while (1) {
        connection_close = FALSE;
        file_not_found = FALSE;
        
        if (recv(socket, recv_buff, config_manager->buffer_size, 0) == -1) {
            /* todo log TIMEOUT!! (ERRNO) */
            break;
        }
        
        struct request *req = malloc(sizeof(struct request));
        if (req == NULL) {
            perror("Error during memory allocation for new object request");
            /* todo log */
            break;
        }
        
        if(parseRequest(recv_buff, req) == -1){
            perror("Non-HTTP request format");
            /* todo log */
            break;
        }
        else if (req->connection != NULL && strcmp(req->connection, "Close")==0) {
            connection_close = TRUE;
            /* todo log */
        }
        
        fd = open_fd(config_manager->document_root, req->URI);
        if (fd == -1) {
            /* todo log */
            /* set 404 page file descriptor */
            file_not_found = TRUE;
        }
        f_length = sizeOfFile(fd);
        f_type = detectFT(req->URI);
        if (f_type == NULL) {
            //todo log
            //conviene fare il break??
            break;
        }
        
        if (connection_close == TRUE && file_not_found == TRUE) {
            sprintf(send_buff, "HTTP/1.1 404 Not found\nConnection: Close\nContent-Type: %s\nContent-Length: %ld\n\n", f_type, f_length);
        }
        else if (connection_close == TRUE) {
            sprintf(send_buff, "HTTP/1.1 200 OK\nConnection: Close\nContent-Type: %s\nContent-Length: %ld\n\n", f_type, f_length);
        }
        else if (file_not_found == TRUE) {
            sprintf(send_buff, "HTTP/1.1 404 Not found\nConnection: Keep-alive\nContent-Type: %s\nContent-Length: %ld\n\n", f_type, f_length);
        }
        else
            sprintf(send_buff, "HTTP/1.1 200 OK\nConnection: Keep-alive\nContent-Type: %s\nContent-Length: %ld\n\n", f_type, f_length);
        
        if(send(socket, send_buff, strlen(send_buff), 0) == -1){
            perror("Error sending data");
            /* todo log (ERRNO) */
            break;
        }
        
        long buf_len = 1;
        while (buf_len > 0) {
            buf_len = read(fd, send_buff, config_manager->buffer_size);
            if (buf_len > 0) {
                if(send(socket, send_buff, buf_len, 0) == -1){
                    perror("Error sending data");
                    /* todo log (ERRNO) */
                    connection_close = TRUE;
                    break;
                }
            }
        }
        if (connection_close == TRUE) {
            break;
        }
        free(req);
    }
    
    close(fd);
    close(socket);
    pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <path_of_config>.\n", argv[0]);
        
        return EXIT_FAILURE;
    }
    
    config_manager = loadConfigFile(argv[1]);
    
    if (!isAValidConfig(*config_manager)) {
        fprintf(stderr, "Config initialization error. Check config content and try again.\n");
        
        return EXIT_FAILURE;
    }
    
    logMessage("ERROR", "Errore porca troia!");
    
    int listen_sock, connection_sock;
    struct sockaddr_in server_addr, client_addr;
    socklen_t length;
    
    pthread_t t_id;
    pthread_attr_t t_attr;
    
    listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock == -1) {
        perror("Error while creating listening socket");
        // todo log
        exit(EXIT_FAILURE);
    }
    
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(config_manager->port_number);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    int reuse = 1;
    if(setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int)) == -1){
        perror("Error during configuration of listening socket");
        // todo log
        exit(EXIT_FAILURE);
    }
    if(bind(listen_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1){
        perror("Error during binding of listening socket");
        // todo log
        exit(EXIT_FAILURE);
    }
    if(listen(listen_sock, config_manager->backlog_size) == -1){
        perror("Error while listening");
        // todo log
        exit(EXIT_FAILURE);
    }
    
    pthread_attr_init(&t_attr);
    
    while (1) {
        
        connection_sock = accept(listen_sock, (struct sockaddr *)&client_addr, &length);
        if (connection_sock == -1) {
            perror("Error establishing new connection");
            // todo log
            continue;
        }
        int arg = connection_sock;
        if(pthread_create(&t_id, &t_attr, threadJob, &arg) != 0){
            perror("Error creating the thread");
            // todo log
            continue;
        }
        
    }
    
    close(listen_sock);
     
    
    return 0;
}
